<template>
  <q-page class="q-pa-md">
    <div class="q-pb-md text-h5">工作流展示 <span class="text-subtitle1">工作流的图形化展示DEMO</span></div>
    <div class="q-pa-sm shadow-2 q-mb-md items-center">
      <div class="row q-col-gutter-sm">
        <div class="col-xs-8 col-sm-6 col-md-5 col-lg-3 row q-col-gutter-sm">
          <div class="col-4 text-right searchbar-label">数据选择</div>
          <div class="col-8">
            <simple-filter-select :options="$d.wfrDemoList" option-label="name" option-value="value"
              :model-value="selectedDemoName" @update:model-value="onSelectedDemoChanged" :use-filter="false"
              :clearable="false" :disable="loadingState" />
          </div>
        </div>
        <div class="col-xs-8 col-sm-6 col-md-5 col-lg-3">
          <q-btn color="primary" v-if="pageMode=='view'" @click="pageMode = 'edit'">转编辑</q-btn>
          <q-btn color="primary" v-if="pageMode!='view'" @click="pageMode = 'view'">转查看</q-btn>
        </div>
      </div>
    </div>
    <div class="shadow-2" id="the-canvas-div" style="position: relative;">
      <canvas id="the-canvas" :width="canvasWidth" :height="canvasHeight" class="canvas-class" />
      <q-inner-loading :showing="loadingState"></q-inner-loading>
      <!-- 用于测量box中的text块的高度的div，以便计算每个box的x，y坐标 -->
      <div id="div-for-measurement" :style="styleOfDivForMeasurement"></div>
      <div v-for="wd in wfrnDispListWithoutAggr" :key="wd.wfrnId" :style="wd.styleObj"
        :class="getClassForWfrnBox(wd)" @click="onNodeClicked(wd.wfrnId)">
        <div v-if="wd.type==0 || wd.type==2">
          <q-icon v-if="wd.wfrnState==2" name="mdi-check-circle" size="sm" title="已完成" color="positive"></q-icon>
          {{wd.type == 0 ? '开始' : '结束'}}
        </div>
        <div v-else>
          <div style="padding: 0 3px">
            <q-icon v-if="wd.wfrnState==0" name="mdi-dots-horizontal-circle" size="sm" title="未开始"
              color="grey"></q-icon>
            <q-icon v-if="wd.wfrnState==1" name="mdi-chevron-right-box" size="sm" title="进行中" color="primary"></q-icon>
            <q-icon v-if="wd.wfrnState==2" name="mdi-check-circle" size="sm" title="已完成" color="positive"></q-icon>
            <q-icon v-if="wd.hasDelay" name="mdi-alert" size="sm" title="存在延期" color="warning"></q-icon>
            <q-icon v-if="wd.hasAttachment" name="library_books" size="sm" title="已上传附件" color="primary"></q-icon>
            <q-icon v-if="wd.hasProgressInfo" name="flag" size="sm" title="有进展记录" color="positive"></q-icon>
          </div>
          <div style="padding:0 3px; border-top: 1px solid grey">
            {{wd.text}}
          </div>
          <div style="padding:0 3px; border-top: 1px solid grey">
            经办: {{wd.pic}}
          </div>
          <div v-if="wd.wfrnState>0" style="padding:0 3px; border-top: 1px solid grey">
            开始: {{wd.beginDt}}
          </div>
          <div v-if="wd.wfrnState==2" style="padding:0 3px; border-top: 1px solid grey">
            完成: {{wd.finishDt}}
          </div>
          <div v-if="wd.wfrnState==1" style="padding:0 3px; border-top: 1px solid grey">
            期限: {{wd.deadlineDt}}
          </div>
        </div>
      </div>
    </div>
  </q-page>
</template>

<script lang="ts">
export default {
  name: 'WfrChartDemo',
}
</script>

<script lang="ts" setup>
import {computed, nextTick, reactive, ref} from 'vue'
import _ from 'lodash'

// 工作流节点信息
interface WfrnForChart {
  // ID，唯一
  wfrnId: number,
  // 是否聚合节点
  isAggregationNode: boolean,
  // 节点显示文本
  wfrnText: string,
  // 状态 0未轮到 1已轮到（未处理）2-已处理
  wfrnState: number,
  // 是否有延迟
  hasDelay: boolean,
  // 是否有附件
  hasAttachment: boolean,
  // 是否有进展信息
  hasProgressInfo: boolean,
  // 处理人，姓名(工号)
  picDisp: string,
  // 开始日期（节点开始之后呈现这个信息）
  beginDt: Date | null,
  // 完成期限（开始之后，完成之前，呈现这个信息）
  deadline: Date | null,
  // 节点完成后呈现这个信息
  finishDt: Date | null,
  // 显示信息，网格Row
  gridRow: number | null,
  // 显示信息，网格Col
  gridCol: number | null,
  // 节点宽度，null表示使用默认值
  nodeWidth: number | null,
  // 父节点
  parents: number[] | null
}

//工作流信息
interface WfrForChart {
  // 节点集合
  nodes: WfrnForChart[]
}

// 节点样式对象
interface WfrnBoxStyleObj {
  width: string,
  left?: string,
  top?: string,
  display?: string,
  'background-color'?: string,
  color?: string
}

// 工作流节点信息的包装
interface WfrnDisp {
  styleObj: WfrnBoxStyleObj, //HTML样式
  type: number, // 0开始节点 1普通节点 2结束节点，普通节点的话下面这些信息才有意义
  wfrnId: number, //ID，处理点击事件时候用到，开始(-1)，结束(-2)
  isAggregationNode: boolean, //是否聚合节点
  xc: number, //根据后端指定的列计算出节点的X轴中心点
  yc: number, //根据后端指定的行计算出节点的Y轴中心点
  text?: string, //文本
  wfrnState?: number,  //状态 0未轮到 1已轮到（未处理）2-已处理
  hasDelay?: boolean, //存在延期
  hasAttachment?: boolean, //有附件
  hasProgressInfo?: boolean, //有进展信息
  pic?: string, //经办人显示
  beginDt?: string, //开始日期
  finishDt?: string, //结束日期
  deadlineDt?: string //期限日期
  parents: number[] | null, //父节点(可以有多个)
}

// 定义网格宽高
const GridWidth = 200
const GridHeight = 160

//默认节点宽度
const defBoxWidth = 180

//开始/结束节点的宽高
const startEndBoxWidth = 85
const startEndBoxHeight = 30

//画布的内边距
const canvasPaddingX = 20
//开始/结束节点的行高调整值，相当于调整首行和尾行的行高
const canvasBeginEndYAdj = -80
const canvasWidth = ref(100)
const canvasHeight = ref(100)

const widthOfDivForMeasurement = ref(0)
const styleOfDivForMeasurement = computed(() => {
  return {
    position: 'absolute',
    top: 0,
    left: 0,
    width: widthOfDivForMeasurement.value,
    visibility: 'hidden',
    padding: '0 3px',
  }
})


const selectedDemoName = ref('0')

// 注意！这个是DEMO，真实情况应当用useDetailHelper来处理页面状态/导航等功能
const loadingState = ref(false)
const pageMode = ref('view') // ‘view'/'create'/'edit'

const getClassForWfrnBox = function (wfrn: WfrnDisp) {
  if (wfrn.type == 0 || wfrn.type == 2) {
    return 'wfrn-box wfrn-box-start-or-end'
  }
  if (pageMode.value == 'view') {
    return 'wfrn-box pointable'
  } else {
    return 'wfrn-box'
  }
}

const $d = reactive({
  // 表单数据
  wfrDemoList: [
    {name: '0 空节点', value: '0'},
    {name: '1-0 单节点未开始', value: '1-0'},
    {name: '1-1 单节点已开始', value: '1-1'},
    {name: '1-2 单节点已完成', value: '1-2'},
    {name: '2-1 两串联节点', value: '2-1'},
    {name: '2-2 两并联节点', value: '2-2'},
    {name: '3 串联+并联', value: '3'},
    {name: '4 综合示例', value: '4'},
    {name: '不存在', value: 'NotExists'},
  ],
  wfrnDispList: [] as WfrnDisp[],
})

const wfrnDispListWithoutAggr = computed(() => {
  return _.filter($d.wfrnDispList, (n) => !n.isAggregationNode)
})

// DEMO数据变化，重新加载
const onSelectedDemoChanged = function (value: string) {
  selectedDemoName.value = value
  loadWfrData()
}

// 测量Box的高度
const measureBoxTextHeight = function (nodeInfo: WfrnDisp) {
  if (nodeInfo.type == 0 || nodeInfo.type == 2) {
    return 30
  }
  const divForMeasurement: HTMLDivElement = <HTMLDivElement>document.getElementById('div-for-measurement')
  divForMeasurement.style.width = nodeInfo.styleObj.width
  divForMeasurement.innerText = nodeInfo.text ?? ''
  let totalHeight = 2 + 24 // 上border + 标题图标
  totalHeight += 1 + divForMeasurement.clientHeight //文本
  totalHeight += 1 + 22 // 经办人
  //开始时间+完成时间
  if (nodeInfo.wfrnState == 2) {
    totalHeight += 1 + 22 + 1 + 22
  }
  //开始时间+期限
  else if (nodeInfo.wfrnState == 1) {
    totalHeight += 1 + 22 + 1 + 22
  }
  // 下border
  totalHeight += 2
  return totalHeight
}

const fmtDate = $f.fmt.dtFmtDateOnly

const handleWfrDemoData = function (data: WfrForChart) {
  //清掉已有节点数据
  $d.wfrnDispList.splice(0)
  //增加开始节点
  $d.wfrnDispList.push({
    styleObj: {
      width: startEndBoxWidth + 'px', //CAL(2) 后面再计算left
      top: (0.5 * (GridHeight + canvasBeginEndYAdj) - startEndBoxHeight / 2) + 'px',
      display: 'none', //CAL(3) 计算好之后再一起把这些显示出来
    },
    type: 0, //开始节点（固定）
    wfrnId: -1, //开始节点（固定）
    isAggregationNode: false, //肯定非聚合节点啊
    xc: 0, //CAL(4) 后面计算更新
    yc: 0.5 * (GridHeight + canvasBeginEndYAdj),
    parents: [], //顶级节点，无父节点
    wfrnState: 0, //CAL(6) 如果有节点的状态>0了，就把起始节点的状态更新为2
  })
  _.forEach(data.nodes, (node) => {
    let boxWidth = node.nodeWidth ?? defBoxWidth
    let xc = GridWidth * ((node.gridCol ?? 1) - 0.5) + canvasPaddingX
    let yc = GridHeight * ((node.gridRow ?? 1) + 0.5) + canvasBeginEndYAdj
    let wfrnDisp: WfrnDisp = {
      styleObj: {
        width: boxWidth + 'px', //CAL(7) CAL(8) top left后面更新
        display: 'none', //CAL(3) 全部计算好之后再一起把这些显示出来
      },
      type: 1, //普通节点（固定）
      wfrnId: node.wfrnId,
      isAggregationNode: node.isAggregationNode,
      xc: xc,
      yc: yc,
      text: node.wfrnText,
      wfrnState: node.wfrnState,
      hasDelay: node.hasDelay,
      hasAttachment: node.hasAttachment,
      hasProgressInfo: node.hasProgressInfo,
      pic: node.picDisp,
      beginDt: node.beginDt ? fmtDate(node.beginDt) : '',
      finishDt: node.finishDt ? fmtDate(node.finishDt) : '',
      deadlineDt: node.deadline ? fmtDate(node.deadline) : '',
      parents: node.parents ?? [-1], //如果没有上级节点，则让它指向开始节点
    }
    let boxHeight = measureBoxTextHeight(wfrnDisp)
    wfrnDisp.styleObj.left = (xc - boxWidth / 2) + 'px' //CAL(7)
    wfrnDisp.styleObj.top = (yc - boxHeight / 2) + 'px' //CAL(8)
    $d.wfrnDispList.push(wfrnDisp)
  })

  //增加结束节点
  $d.wfrnDispList.push({
    styleObj: {
      width: startEndBoxWidth + 'px', //CAL(9) CAL(10) 后面再计算top和left
      display: 'none', //CAL(3) 计算好之后再一起把这些显示出来
    },
    type: 2, //结束节点（固定）
    wfrnId: -2, //结束节点（固定）
    isAggregationNode: false, //肯定非聚合节点啊
    xc: 0, //CAL(11) 后面计算更新
    yc: 0, //CAL(12) 后面计算更新
    parents: [], //CAL(13) 后面计算，指向全部没有其它节点指向的节点
    wfrnState: 0, //CAL(14) 如果全部节点的状态为2，那就将这个更新为2
  })

  //CAL(13) 如果一个节点没有任何其它节点指向它，那就让结束节点指向它
  let allWfrnIds: number[] = _.filter(_.map($d.wfrnDispList, 'wfrnId'), (id) => id != -2) //还得把自己给排除掉
  let allWfrnParentIds: number[] = _.union(..._.map($d.wfrnDispList, 'parents'))
  let bottomNodeIds: number[] = _.without(allWfrnIds, ...allWfrnParentIds)
  let wfrnEnd: WfrnDisp = <WfrnDisp>_.find($d.wfrnDispList, {'wfrnId': -2})
  wfrnEnd.parents = bottomNodeIds

  //CAL(6) 起始节点的状态
  let wfrnStart: WfrnDisp = <WfrnDisp>_.find($d.wfrnDispList, {'wfrnId': -1})
  wfrnStart.wfrnState = _.some($d.wfrnDispList, (n) => n.wfrnState == 1 || n.wfrnState == 2) ? 2 : 0

  //CAL(14) 结束节点的状态
  wfrnEnd.wfrnState = _.every($d.wfrnDispList, (n) => n.type == 2 || n.wfrnState == 2) ? 2 : 0

  //计算起始节点的中心x (这里要考虑一个特例，只有开始节点和结束节点的情况）
  if (data.nodes.length == 0) {
    wfrnStart.xc = 0.5 * GridWidth + canvasPaddingX // CAL(4)
    wfrnStart.styleObj.left = (wfrnStart.xc - startEndBoxWidth / 2) + 'px' //CAL(2)
  } else {
    let nodesPointToStart: WfrnDisp[] = _.filter($d.wfrnDispList, (n) => _.includes(n.parents, -1))
    let xcsOfNodesPointToStart: number[] = _.map(nodesPointToStart, (n) => n.xc)
    let minXcOfNodesPointToStart: number = <number>_.min(xcsOfNodesPointToStart)
    let maxXcOfNodesPointToStart: number = <number>_.max(xcsOfNodesPointToStart)
    wfrnStart.xc = (minXcOfNodesPointToStart + maxXcOfNodesPointToStart) / 2 // CAL(4)
    wfrnStart.styleObj.left = (wfrnStart.xc - startEndBoxWidth / 2) + 'px' // CAL(2)
  }

  //计算结束节点的中心x，y
  let nodesPointedByEnd: WfrnDisp[] = _.filter($d.wfrnDispList, (n) => _.includes(bottomNodeIds, n.wfrnId))
  let xcsOfNodesPointedByEnd: number[] = _.map(nodesPointedByEnd, (n) => n.xc)
  let minXcOfNodesPointedByEnd: number = <number>_.min(xcsOfNodesPointedByEnd)
  let maxXcOfNodesPointedByEnd: number = <number>_.max(xcsOfNodesPointedByEnd)
  wfrnEnd.xc = (minXcOfNodesPointedByEnd + maxXcOfNodesPointedByEnd) / 2 // CAL(11)
  let ycsOfNodesPointedByEnd: number[] = _.map(nodesPointedByEnd, (n) => n.yc)
  let maxYcOfNodesPointedByEnd: number = <number>_.max(ycsOfNodesPointedByEnd)
  //这里需要考虑一个特例，只有开始节点和结束节点的情况
  if (data.nodes.length == 0) {
    wfrnEnd.yc = maxYcOfNodesPointedByEnd + GridHeight + canvasBeginEndYAdj //CAL(12)
  } else {
    wfrnEnd.yc = maxYcOfNodesPointedByEnd + GridHeight + canvasBeginEndYAdj / 2 //CAL(12)
  }
  wfrnEnd.styleObj.left = (wfrnEnd.xc - startEndBoxWidth / 2) + 'px' //CAL(10)
  wfrnEnd.styleObj.top = (wfrnEnd.yc - startEndBoxHeight / 2) + 'px' //CAL(9)

  //计算Canvas的宽和高
  canvasWidth.value = <number>_.max(_.map($d.wfrnDispList, (n) => n.xc)) + GridWidth / 2 + canvasPaddingX
  canvasHeight.value = <number>_.max(_.map($d.wfrnDispList, (n) => n.yc)) + (GridHeight + canvasBeginEndYAdj) / 2

  //将全部非聚合节点display出来 CAL(3)
  _.forEach($d.wfrnDispList, (n) => {
    if (!n.isAggregationNode) {
      n.styleObj.display = 'block'
    }
  })
}

// 加载数据
const loadWfrData = function () {
  loadingState.value = true
  $f.axios
    .get<WfrForChart>('/misc-test/wfrn-chart-test/' + selectedDemoName.value)
    .then((result) => {
      console.log(result.data)
      handleWfrDemoData(result.data)
      nextTick(() => {
        drawCanvas()
      })
    })
    .catch((err) => {
      $f.ui.showNotifyFailed($f.getAxiosErrMsg(err))
    })
    .finally(() => {
      loadingState.value = false
    })
}

const drawCanvas = function () {
  const theCanvas: HTMLCanvasElement = <HTMLCanvasElement>document.getElementById('the-canvas')
  const ctx: CanvasRenderingContext2D = <CanvasRenderingContext2D>theCanvas.getContext('2d')
  //清除画布
  ctx.lineCap = 'round'
  ctx.clearRect(0, 0, canvasWidth.value, canvasHeight.value)


  //别删除这段注释，网格线用于开发调试
  //画网格线
  ctx.beginPath()
  ctx.strokeStyle = '#EEE'
  ctx.lineWidth = 1
  //横
  let y = canvasBeginEndYAdj
  while (y <= canvasHeight.value - canvasBeginEndYAdj) {
    ctx.moveTo(0, y)
    ctx.lineTo(canvasWidth.value, y)
    y += GridHeight
  }
  //竖
  let x = canvasPaddingX
  while (x <= canvasWidth.value - canvasPaddingX) {
    ctx.moveTo(x, 0)
    ctx.lineTo(x, canvasHeight.value)
    x += GridWidth
  }
  ctx.stroke()


  //遍历每个节点，让它跟父节点连线
  ctx.beginPath()
  ctx.strokeStyle = '#808080'
  ctx.lineWidth = 2
  _.forEach($d.wfrnDispList, (n) => {
    if (n.parents) {
      _.forEach(n.parents, (pnId) => {
        let pn: WfrnDisp | null = <WfrnDisp | null>_.find($d.wfrnDispList, {'wfrnId': pnId})
        if (pn) {
          let middleY
          if (n.wfrnId == -2) { //结束节点的特殊处理
            middleY = n.yc - (GridHeight + canvasBeginEndYAdj) / 2
          } else if (pn.wfrnId == -1) { //开始节点的特殊处理
            middleY = pn.yc + (GridHeight + canvasBeginEndYAdj) / 2
          } else if (pn.isAggregationNode) { //对上级是聚合节点的特殊处理
            middleY = pn.yc + 20
          } else if (n.isAggregationNode) { //对自己是聚合节点的特殊处理
            middleY = n.yc - 20
          } else {
            //如果本节点有多个父节点，就把本节点当成一个合拢节点，使得middleY靠近它，否则middleY靠近它的父节点
            if ((n.parents?.length ?? 0) > 1) {
              middleY = n.yc - GridHeight / 2
            } else {
              middleY = pn.yc + GridHeight / 2
            }
          }
          ctx.moveTo(n.xc, n.yc)
          ctx.lineTo(n.xc, middleY)
          ctx.lineTo(pn.xc, middleY)
          ctx.lineTo(pn.xc, pn.yc)
        }
      })
    }
  })
  ctx.stroke()
}

//判断当前是否没有文本被选中，如果有，则不认为是点击事件
function isNoTextSelected() {
  const selection: Selection | null = window.getSelection()
  return selection == null || selection.isCollapsed
}

const onNodeClicked = function (wfrnId: number) {
  //用nextTick似乎不太牢靠，还是等个30ms，再来判断是否有文本被选中比较准确
  setTimeout(() => {
    // 查看状态下，且没有文本被选中，才认为是点击
    if (pageMode.value == 'view' && isNoTextSelected()) {
      //todo：启用快速查看
      console.log('节点被点击: ' + wfrnId)
    }
  }, 30)
}

loadWfrData()

</script>
<style lang="sass" scoped>
.wfrn-box
  border: 2px solid grey
  position: absolute
  background-color: #FFFDF0

.wfrn-box.pointable:hover
  box-shadow: 0 0 7px grey
  cursor: pointer

.wfrn-box-start-or-end
  border-radius: 15px
  line-height: 26px
  text-align: center
  width: 85px

.canvas-class
  position: relative
</style>
